﻿namespace Webinator.Impls
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using System.Text.RegularExpressions;
    using System.Threading;
    using Condition;
    using Condition.Base;
    using Enums;
    using ExtensionMethods;
    using HtmlAgilityPack;
    using Interfaces;
    using Toolboxes;
    using Util;

    /// <summary>
    /// Handles <c>ICondition</c> Waits, Delays, and Repeats.
    /// </summary>
    [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed. Suppression is OK here.")]
    public class ConditionInvoker
    {
        private readonly IAjax _ajax;
        private readonly IJavascript _javascript;
        private readonly IWebManager _webManager;
        private readonly ICore _core;

        public ConditionInvoker(IAjax ajaxToolbox, IJavascript javascriptToolbox, IWebManager webManager, ICore core)
        {
            _ajax = ajaxToolbox;
            _javascript = javascriptToolbox;
            _webManager = webManager;
            _core = core;
        }

        public ConditionInvoker(IAjax ajaxToolbox, IJavascript javascriptToolbox, IWebManager webManager)
        {
            _ajax = ajaxToolbox;
            _javascript = javascriptToolbox;
            _webManager = webManager;
            _core = null;
        }

        public bool WaitForSuccess(Func<bool> action, int timeoutInMilliseconds)
        {
            if (action == null) return false;

            bool success;
            const int PollRate = 250;
            var maxTries = timeoutInMilliseconds / PollRate;
            int tries = 0;
            do
            {
                success = action();
                tries++;
                if (!success && tries <= maxTries)
                {
                    Thread.Sleep(PollRate);
                }
            }
            while (!success && tries < maxTries);
            return success;
        }

        public bool ExecuteConditionalAction(Action action, ICondition condition, ConditionType overrideConditionType = ConditionType.None)
        {
            if (condition == null)
            {
                if (action != null) action();
                return true;
            }

            bool success = true;

            var baseCondition = condition as ConditionBase;
            if (baseCondition != null)
            {
                ConditionType type = baseCondition.Type;
                if (overrideConditionType != ConditionType.None) type = overrideConditionType;
                success = InvokeAction(action, condition, type, baseCondition.TimeoutInMilliseconds);
                if (baseCondition.ThrowException && !success) throw new Exception("Condition failed.");
            }

            return success;
        }

        private bool InvokeAction(Action action, ICondition condition, ConditionType conditionType, int timeout)
        {
            var waitType = (WaitUntilType)Enum.Parse(typeof(WaitUntilType), Regex.Match(condition.GetType().ToString(), @"[^\.]*$").Value);
            Action safeAction = () => { if (action != null) action(); };
            Func<bool> successIndicator;
            Action preAction = () => { };
            Action postAction = () => { };

// ReSharper disable PossibleNullReferenceException
// The condition is already guaranteed not to be null when downcasting
            switch (waitType)
            {
                case WaitUntilType.Iterations: // Repeat condition only
                    var iteration = condition as Iterations;
                    int iterations = 0;
                    successIndicator = () =>
                    {
                        bool iterationsReached = false;
                        iterations++;
                        if (iterations == iteration.NumberOfIterations)
                        {
                            iterationsReached = true;
                        }

                        return iterationsReached;
                    };
                    break;
                case WaitUntilType.TimeElapsed: // Wait/Delay condition only
                    var timeElapsed = condition as TimeElapsed;
                    successIndicator = () => { Thread.Sleep(timeElapsed.Milliseconds); return true; };
                    break;
                case WaitUntilType.AjaxOrPostCompleted:
                    bool ajaxOrPostActive = false;
                    preAction = () => { _javascript.BeginListeningForPageSubmit(); _ajax.BeginListeningForAjax(); };
                    successIndicator = () =>
                    {
                        if (_javascript.PageIsSubmitting() && !ajaxOrPostActive)
                        {
                            _javascript.WaitForPageToFinishSubmitting();
                            ajaxOrPostActive = true;
                        }

                        if (_ajax.AjaxIsRunning() && !ajaxOrPostActive)
                        {
                            _ajax.WaitForAjaxToComplete();
                            ajaxOrPostActive = true;
                        }

                        return ajaxOrPostActive;
                    };
                    postAction = () => { _ajax.StopListeningForAjax(); _javascript.StopListeningForPageSubmit(); };
                    break;
                case WaitUntilType.NetworkTrafficSilent:
                    var networkSilent = condition as NetworkTrafficSilent;
                    int gracePeriod = 0;
                    var id = _webManager.Process.Id;
                    var processName = _webManager.Process.ProcessName;
                    var instances = Process.GetProcessesByName(processName).Length;
                    var counter = new PerformanceCounter("Process", "IO Data Bytes/sec", GetInstanceName(id));
                    counter.NextValue(); // First value is always zero

                    successIndicator = () =>
                    {
                        bool silent = false;

                        var newInstances = Process.GetProcessesByName(processName).Length;
                        if (newInstances != instances)
                        {
                            counter = new PerformanceCounter("Process", "IO Data Bytes/sec", GetInstanceName(id));
                            counter.NextValue(); // First value is always zero
                            instances = newInstances;
                        }

                        if (Math.Abs(counter.NextValue() - 0) < float.Epsilon)
                        {
                            gracePeriod += 250;
                            if (gracePeriod == networkSilent.GracePeriod) silent = true;
                        }
                        else
                        {
                            gracePeriod = 0;
                        }

                        return silent;
                    };
                    break;
                case WaitUntilType.Displayed:
                    var displayed = condition as Displayed;
                    successIndicator = () => _webManager.IsVisible(displayed.Locator, 250);
                    break;
                case WaitUntilType.NotDisplayed:
                    var notDisplayed = condition as NotDisplayed;
                    successIndicator = () => !_webManager.IsVisible(notDisplayed.Locator, 250);
                    break;
                case WaitUntilType.Exists:
                    var exists = condition as Exists;
                    successIndicator = () => _webManager.Exists(exists.Locator, 250);
                    break;
                case WaitUntilType.NotExists:
                    var notExists = condition as NotExists;
                    successIndicator = () => !_webManager.Exists(notExists.Locator, 250);
                    break;
                case WaitUntilType.UrlChanged:
                    var urlChanges = condition as UrlChanged;
                    var oldUrl = _webManager.Url();
                    successIndicator = () => _webManager.Url().StringMatch(urlChanges.Comparison, oldUrl, urlChanges.LookForText);
                    break;
                case WaitUntilType.WindowTitleChanged:
                    var titleChanges = condition as WindowTitleChanged;
                    var oldTitle = _webManager.Title();
                    successIndicator = () => _webManager.Title().StringMatch(titleChanges.Comparison, oldTitle, titleChanges.LookForText);
                    break;
                case WaitUntilType.NodeChanged:
                    var attributeBase = condition as NodeChanged;
                    var xpath = XPathToolbox.GetXpathFromLocator(attributeBase.Locator);
                    var oldNodes = XPathToolbox.GetNodes(_webManager.PageSource(), xpath);
                    successIndicator = () => NodeChanged(oldNodes.ToArray(), attributeBase);
                    break;
                case WaitUntilType.CollectionSize:
                    var collectionSize = condition as CollectionSize;
                    if (collectionSize.Locator.Use != Use.All)
                    {
                        throw new Exception(@"Locator does not reference a collection, did you forget the ""Use.All"" parameter?");
                    }

                    var originalSize = _webManager.GetNodeCount(collectionSize.Locator);
                    successIndicator = () => CollectionSizeMatch(collectionSize, originalSize);
                    break;
                case WaitUntilType.DialogAppears:
                    var dialogAppears = condition as DialogAppears;
                    successIndicator = () =>
                    {
                        var dialogs = GetDialogHandles(dialogAppears.LookForTitle, dialogAppears.Comparison, dialogAppears.Parent);
                        bool found = dialogs.Any();
                        return found;
                    };
                    break;
                case WaitUntilType.WindowAppears:
                    var windowAppears = condition as WindowAppears;
                    successIndicator = () =>
                    {
                        IEnumerable<string> values = null;
                        switch (windowAppears.SwitchBy)
                        {
                            case SwitchBy.Url:
                                values = _core.WindowUrls();
                                break;
                            case SwitchBy.Title:
                                values = _core.WindowTitles();
                                break;
                        }

                        return values != null && values.Any(value => value.StringMatch(windowAppears.Comparison, windowAppears.Value));
                    };
                    break;
                case WaitUntilType.WindowDisappears:
                    successIndicator = () => !_core.CurrentWindowIsValid();
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
// ReSharper restore PossibleNullReferenceException
            }

            var success = false;
            switch (conditionType)
            {
                case ConditionType.Delay:
                    Func<bool> delayAction = () => successIndicator();
                    preAction();
                    success = WaitForSuccess(delayAction, timeout);
                    if (success) safeAction();
                    postAction();
                    break;
                case ConditionType.Wait:
                    Func<bool> waitAction = () => successIndicator();
                    preAction();
                    safeAction();
                    success = WaitForSuccess(waitAction, timeout);
                    postAction();
                    break;
                case ConditionType.Repeat:
                    Func<bool> repeatAction = () => { safeAction(); return successIndicator(); };
                    preAction();
                    success = WaitForSuccess(repeatAction, timeout);
                    postAction();
                    break;
            }

            return success;
        }

        private string GetInstanceName(int processId)
        {
            string instanceName = Process.GetProcessById(processId).ProcessName;
            bool found = false;
            if (!string.IsNullOrEmpty(instanceName))
            {
                Process[] processes = Process.GetProcessesByName(instanceName);
                if (processes.Length > 0)
                {
                    int i = 0;
                    foreach (Process p in processes)
                    {
                        instanceName = i > 0 ? string.Format("{0}#{1}", p.ProcessName, i) : p.ProcessName;
                        if (PerformanceCounterCategory.CounterExists("ID Process", "Process"))
                        {
                            PerformanceCounter counter = new PerformanceCounter("Process", "ID Process", instanceName);
                            if (processId == counter.RawValue)
                            {
                                found = true;
                                break;
                            }
                        }

                        i++;
                    }
                }
            }

            if (!found) instanceName = string.Empty;

            return instanceName;
        }

        private bool CollectionSizeMatch(CollectionSize collectionSize, int oldCount)
        {
            bool success = false;
            var newSize = _webManager.GetNodeCount(collectionSize.Locator);

            switch (collectionSize.SizeIs)
            {
                case SizeIs.EqualTo:
                    if (newSize == collectionSize.X) success = true;
                    break;
                case SizeIs.GreaterThan:
                    if (newSize > collectionSize.X) success = true;
                    break;
                case SizeIs.LessThan:
                    if (newSize < collectionSize.X) success = true;
                    break;
                case SizeIs.IncreasedBy:
                    if (newSize >= (oldCount + collectionSize.X)) success = true;
                    break;
                case SizeIs.DecreasedBy:
                    if (newSize <= (oldCount - collectionSize.X)) success = true;
                    break;
            }

            return success;
        }

        private bool NodeChanged(HtmlNode[] oldNodes, NodeChanged nodeChanged)
        {
            var xpath = XPathToolbox.GetXpathFromLocator(nodeChanged.Locator);
            var newNodes = XPathToolbox.GetNodes(_webManager.PageSource(), xpath);
            bool conditionMet = false;
            switch (nodeChanged.Locator.Use)
            {
                case Use.One:
                    conditionMet = AttributesChanged(oldNodes[nodeChanged.Locator.Index], newNodes[nodeChanged.Locator.Index], nodeChanged);
                    break;
                case Use.Any:
                    for (var i = 0; i < newNodes.Count; i++)
                    {
                        conditionMet = AttributesChanged(oldNodes[i], newNodes[i], nodeChanged);
                        if (conditionMet) break;
                    }

                    break;
                case Use.All:
                    var passedCount = 0;
                    for (var i = 0; i < newNodes.Count; i++)
                    {
                        if (AttributesChanged(oldNodes[i], newNodes[i], nodeChanged)) passedCount++;
                    }

                    if (passedCount == newNodes.Count) conditionMet = true;

                    break;
            }

            return conditionMet;
        }

        private bool AttributesChanged(HtmlNode oldNode, HtmlNode newNode, NodeChanged nodeChanged)
        {
            bool passed = false;

            // Text isn't precisely an attribute, so we have to test for that separately
            if (nodeChanged.AttributeIs != AttributeIs.Text)
            {
                string attribute = nodeChanged.AttributeIs == AttributeIs.Custom
                                       ? nodeChanged.CustomAttribute
                                       : nodeChanged.AttributeIs.ToString().ToLower();
                switch (nodeChanged.AttributeIs)
                {
                    case AttributeIs.Any:
                        if (nodeChanged.ChangeType == NodeChangeType.AttributeAdded) passed = newNode.Attributes.Count > oldNode.Attributes.Count;
                        if (nodeChanged.ChangeType == NodeChangeType.AttributeRemoved) passed = newNode.Attributes.Count < oldNode.Attributes.Count;
                        if (nodeChanged.ChangeType == NodeChangeType.AttributeChanged)
                        {
                            foreach (var newAttribute in newNode.Attributes)
                            {
                                passed = newAttribute.Value.NormalizeSpace().Trim().StringMatch(nodeChanged.Comparison, oldNode.Attributes[newAttribute.Name].Value.NormalizeSpace().Trim(), nodeChanged.LookForValue);
                                if (passed) break;
                            }
                        }

                        break;
                    default:
                        if (nodeChanged.ChangeType == NodeChangeType.AttributeAdded) passed = newNode.Attributes[attribute] != null;
                        if (nodeChanged.ChangeType == NodeChangeType.AttributeRemoved) passed = newNode.Attributes[attribute] == null;
                        if (nodeChanged.ChangeType == NodeChangeType.AttributeChanged) passed = newNode.Attributes[attribute].Value.NormalizeSpace().Trim().StringMatch(nodeChanged.Comparison, oldNode.Attributes[attribute].Value.NormalizeSpace().Trim(), nodeChanged.LookForValue);
                        break;
                }
            }
            else
            {
                if (nodeChanged.ChangeType == NodeChangeType.AttributeAdded) passed = true;
                if (nodeChanged.ChangeType == NodeChangeType.AttributeRemoved) passed = false;
                if (nodeChanged.ChangeType == NodeChangeType.AttributeChanged) passed = newNode.InnerHtml.StringMatch(nodeChanged.Comparison, oldNode.InnerHtml, nodeChanged.LookForValue);
            }

            return passed;
        }

        private IEnumerable<IntPtr> GetDialogHandles(string title, Comparison comparison, IntPtr parent)
        {
            List<IntPtr> handles = new List<IntPtr>();

            foreach (IntPtr hWnd in WindowHelper.GetDescendantWindows(parent))
            {
                if (string.IsNullOrEmpty(title))
                {
                    if (WindowHelper.GetWindowClass(hWnd) == "#32770")
                    {
                        handles.Add(hWnd);
                    }
                }
                else
                {
                    var actualTitle = WindowHelper.GetWindowTitle(hWnd);
                    if (WindowHelper.GetWindowClass(hWnd) == "#32770" && actualTitle.StringMatch(comparison, title))
                    {
                        handles.Add(hWnd);
                    }
                }
            }

            return handles.ToList();
        }
    }
}
